/**
 * This file is Copyright © 2008 Software Craftsmen Limited. All Rights Reserved.
 */
package com.softwarecraftsmen.orogeny.execution;

import com.softwarecraftsmen.orogeny.Task;
import com.softwarecraftsmen.orogeny.buildEnvironments.BuildEnvironment;
import com.softwarecraftsmen.orogeny.buildLogs.IndentingBuildLog;
import static com.softwarecraftsmen.orogeny.execution.SuccessOrFailure.Failure;
import static com.softwarecraftsmen.orogeny.execution.SuccessOrFailure.Success;
import com.softwarecraftsmen.orogeny.taskNames.TaskName;
import org.jetbrains.annotations.NotNull;

import static java.lang.System.err;
import static java.lang.System.exit;
import static java.lang.System.currentTimeMillis;
import static java.lang.Thread.currentThread;
import static java.lang.Thread.sleep;
import static java.lang.Thread.MIN_PRIORITY;
import static java.lang.Runtime.getRuntime;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import static java.util.concurrent.TimeUnit.MILLISECONDS;

public class BuildExecuter
{
	private final Map<TaskName, Task> allTasks;
	private final BuildEnvironment buildEnvironment;
	private final TasksExecuteInParallelProcessor tasksExecuteInParallelProcessor;

	public BuildExecuter(final @NotNull Map<TaskName, Task> allTasks, final @NotNull BuildEnvironment buildEnvironment, final @NotNull TasksExecuteInParallelProcessor tasksExecuteInParallelProcessor)
	{
		this.tasksExecuteInParallelProcessor = tasksExecuteInParallelProcessor;
		this.allTasks = new LinkedHashMap<>(allTasks);
		this.buildEnvironment = buildEnvironment;
	}

	@NotNull
	public SuccessOrFailure execute(final @NotNull TaskName taskNameToExecute, final @NotNull IndentingBuildLog indentingBuildLog)
	{
		currentThread().setPriority(MIN_PRIORITY);
		final ExecutorService parallelTaskExecution = tasksExecuteInParallelProcessor.taskThreadPool();
		final boolean isSingleThreaded = tasksExecuteInParallelProcessor.isSingleThreaded();
		try
		{
			final Set<TaskName> remainingTasks = findTasksToExecuteInOrder(taskNameToExecute);
			final Map<TaskName, Future<?>> tasksInProgress = new LinkedHashMap<>();
			final Set<TaskName> completedTasks = new LinkedHashSet<>();
			final int numberOfTasksToExecute = remainingTasks.size();
			final BlockingQueue<TaskCompletionNotification> taskCompletionNotifications = new ArrayBlockingQueue<>(numberOfTasksToExecute);

			while(completedTasks.size() != numberOfTasksToExecute)
			{
				startMoreTasksIfDependantTasksComplete(parallelTaskExecution, completedTasks, tasksInProgress, remainingTasks, taskCompletionNotifications, indentingBuildLog, isSingleThreaded);
				final TaskCompletionNotification taskCompletionNotification = taskCompletionNotifications.poll(100, MILLISECONDS);
				if (taskCompletionNotification == null)
				{
					continue;
				}
				taskCompletionNotification.writeMessage(indentingBuildLog);
				final SuccessOrFailure successOrFailure = taskCompletionNotification.successOrFailure();
				if (successOrFailure.isFailure())
				{
					return successOrFailure;
				}
				taskCompletionNotification.removeFrom(tasksInProgress);
				taskCompletionNotification.addTo(completedTasks);

				sleep(2);
			}
		}
		catch(InterruptedException e)
		{
			return Failure(e);
		}
		finally
		{
			parallelTaskExecution.shutdownNow();
		}
		return Success;
	}

	private void startMoreTasksIfDependantTasksComplete(final ExecutorService parallelTaskExecution, final Set<TaskName> completedTasks, final Map<TaskName, Future<?>> tasksInProgress, final Set<TaskName> remainingTasks, final BlockingQueue<TaskCompletionNotification> taskCompletionNotifications, final IndentingBuildLog indentingBuildLog, final boolean isSingleThreaded)
	{
		final Set<TaskName> tasksStarted = new LinkedHashSet<>();
		for (final TaskName taskName : remainingTasks)
		{
			final Task task = allTasks.get(taskName);
			if (!task.allDependenciesSatisfied(completedTasks))
			{
				continue;
			}
			final RecordingBuildLog buildLog = isSingleThreaded ? new NonRecordingIndentingBuildLog(indentingBuildLog) : new RecordingIndentingBuildLog();
			tasksInProgress.put(taskName, parallelTaskExecution.submit(new Runnable()
			{
				public void run()
				{
					err.printf("Thread %1$s at %2$s executing task %3$s\n", currentThread().getName(), currentTimeMillis(), task);
					SuccessOrFailure successOrFailure = null;
					try
					{
						try
						{
							successOrFailure = task.execute(buildLog, buildEnvironment);
						}
						catch(Error e)
						{
							successOrFailure = Failure(e);
						}
						catch (Exception e)
						{
							successOrFailure = Failure(e);
						}
						catch (Throwable e)
						{
							successOrFailure = Failure(e);
						}
					}
					catch(Throwable e)
					{
						seriousFailureEnsureThreadDeath(e);
					}
					try
					{
						taskCompletionNotifications.put(new TaskCompletionNotification(taskName, successOrFailure, buildLog));
					}
					catch (Throwable e)
					{
						seriousFailureEnsureThreadDeath(e);
					}
					err.printf("Thread %1$s at %2$s executed task %3$s; free memory %4$s\n", currentThread().getName(), currentTimeMillis(), task, getRuntime().freeMemory());
				}

				private void seriousFailureEnsureThreadDeath(final Throwable seriousFailure)
				{
					err.println(seriousFailure);
					exit(1);
				}
			}));
			tasksStarted.add(taskName);
		}
		remainingTasks.removeAll(tasksStarted);
	}

	@NotNull
	private Set<TaskName> findTasksToExecuteInOrder(final @NotNull TaskName taskName)
	{
		final Queue<TaskName> taskNamesInExecutionOrder = new LinkedList<>();
		taskName.selectTask(allTasks).originalDependentTasks(taskNamesInExecutionOrder, allTasks);
		return new LinkedHashSet<>(taskNamesInExecutionOrder);
	}

}
